package com.cx.repository;

import com.cx.config.LettuceRedisAdapter;
import com.cx.entity.RedisEntity;
import com.cx.property.LettuceProperties;
import com.cx.support.AsyncCallback;
import com.cx.utils.BeanHelper;
import com.cx.utils.JsonHelper;
import com.cx.utils.SerializeAdapter;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import io.lettuce.core.api.async.RedisAsyncCommands;
import io.lettuce.core.api.async.RedisKeyAsyncCommands;
import io.lettuce.core.api.async.RedisStringAsyncCommands;
import io.lettuce.core.api.reactive.RedisReactiveCommands;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.*;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springside.modules.utils.collection.CollectionUtil;

import javax.persistence.EntityManager;
import java.io.Serializable;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Default implementation of the {@link org.springframework.data.repository.CrudRepository} interface. This will offer
 * you a more sophisticated interface than the plain {@link EntityManager} .
 * 
 * @author 舒建辉
 * @param <T> the type of the entity to handle
 * @param <ID> the type of the entity's identifier
 */
@Transactional(readOnly=true, rollbackFor = Exception.class)
public class BaseJpaRedisRepositoryImpl<T extends RedisEntity<ID>, ID extends Serializable>
        extends SimpleJpaRepository<T, ID> {

    private static final String ID_MUST_NOT_BE_NULL = "The given id must not be null!";

    private final EntityManager em;

    private final Class<T> clazz;

    private final String clazzName;

    private final LettuceRedisAdapter lettuceRedisAdapter;

    private final LettuceProperties lettuceProperties;

    public BaseJpaRedisRepositoryImpl(Class<T> domainClass, EntityManager entityManager, LettuceRedisAdapter lra) {
        super(domainClass, entityManager);
        this.clazz = domainClass;
        this.clazzName = domainClass.getSimpleName().toLowerCase();
        this.em = entityManager;
        this.lettuceRedisAdapter = lra;
        this.lettuceProperties = SerializeAdapter.lettuceProperties();
    }

    private void deleteByCacheDeling(final Set<String> keys){

        final int keySize = keys.size();
        lettuceRedisAdapter.del(keys.toArray(new String[keySize]));

        final String delingkey = lettuceProperties.cacheLevel2Pre() + "deling:" + clazzName;
        final List<Object> delingCacheKeys = lettuceRedisAdapter.smembers(delingkey);
        if(!CollectionUtils.isEmpty(delingCacheKeys)){
            final List<String> delingCachedKeys = Lists.newArrayListWithCapacity(3);
            delingCacheKeys.iterator().forEachRemaining(delingCacheKey -> {
                delingCachedKeys.addAll(lettuceRedisAdapter.keys(String.valueOf(delingCacheKey)));
            });
            lettuceRedisAdapter.del(delingCachedKeys.toArray(new String[delingCachedKeys.size()]));
        }else{
            final List<String> entityKeys = lettuceRedisAdapter.keys(keyspace(clazzName));
            if(!CollectionUtils.isEmpty(entityKeys)) {
                lettuceRedisAdapter.del(entityKeys.toArray(new String[entityKeys.size()]));
            }
        }
    }

    @Transactional
    @Override
	public void delete(final ID id) {
		Assert.notNull(id, ID_MUST_NOT_BE_NULL);

		try {
			super.delete(id);

            final String key = key(id);
            deleteByCacheDeling(Sets.newHashSet(key));
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	@Transactional
    @Override
	public void delete(final T entity) {
		Assert.notNull(entity, "The entity must not be null!");

		try {
			super.delete(entity);

            final String key = key(entity.getId());
            deleteByCacheDeling(Sets.newHashSet(key));
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	@Transactional
    @Override
	public void delete(Iterable<? extends T> entities) {
		Assert.notNull(entities, "The given Iterable of entities not be null!");

        final Set<String> keys = new HashSet<>();
        final StringBuffer ids = new StringBuffer();
		try {
			entities.iterator().forEachRemaining(entity -> {
                final ID id = entity.getId();
				keys.add(key(id));
				ids.append(id);
			});

            if(keys.size() == 0){
                return;
            }

            this.deleteByJpa(entities);

            deleteByCacheDeling(keys);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	private void deleteByJpa(final Iterable<? extends T> entities){
        Assert.notNull(entities, "The given Iterable of entities not be null!");
        Iterator<? extends T> var2 = entities.iterator();

        while(var2.hasNext()) {
            T entity = var2.next();
            super.delete(entity);
        }
    }

	@Transactional
    @Override
	public void deleteInBatch(Iterable<T> entities) {
		Assert.notNull(entities, "The given Iterable of entities not be null!");

        final Set<String> keys = new HashSet<>();
        final StringBuffer ids = new StringBuffer();
		try {
			entities.iterator().forEachRemaining(entity -> {
                final ID id = entity.getId();
				keys.add(key(id));
				ids.append(id);
			});

            if(keys.size() == 0){
                return;
            }

			super.deleteInBatch(entities);

            deleteByCacheDeling(keys);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	@Transactional
    @Override
	public void deleteAll() {
		try {
			this.deleteAllByJpa();

            final List<String> keys = entityKeys();
			if(keys.size() == 0){
				return;
			}

            deleteByCacheDeling(Sets.newHashSet(keys));
            keys.clear();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

    private void deleteAllByJpa() {
        final Iterator<T> var1 = this.findAll().iterator();

        while(var1.hasNext()) {
            final T element = var1.next();
            super.delete(element);
        }
    }

	@Transactional
    @Override
	public void deleteAllInBatch() {
		try {
			super.deleteAllInBatch();

            final List<String> keys = entityKeys();
			if(keys.size() == 0){
				return;
			}

            deleteByCacheDeling(Sets.newHashSet(keys));
            keys.clear();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

    @Override
	public T findOne(final ID id) {
		Assert.notNull(id, ID_MUST_NOT_BE_NULL);

		try {
			T t = getOnlyOne(key(id), clazz);
			if(null != t) {
				return t;
			}

			t = super.findOne(id);

            if(null == t) {
                return t;
            }

//            lettuceRedisAdapter.hmset(key(t.getId()), BeanHelper.toHash(t));
            lettuceRedisAdapter.setex(key(t.getId()), JsonHelper.serialize(t));

			return t;
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public T getOne(final ID id) {
		Assert.notNull(id, ID_MUST_NOT_BE_NULL);

		try {
			T t = getOnlyOne(key(id), clazz);
			if(null != t) {
				return t;
			}

			t = super.getOne(id);

            if(null == t) {
                return t;
            }

//            lettuceRedisAdapter.hmset(key(t.getId()), BeanHelper.toHash(t));
            lettuceRedisAdapter.setex(key(t.getId()), JsonHelper.serialize(t));

			return t;
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

    @Override
	public boolean exists(final ID id) {
		Assert.notNull(id, ID_MUST_NOT_BE_NULL);

        try {
            final Boolean exists = lettuceRedisAdapter.exists(key(id));
			return (BooleanUtils.toBoolean(exists)) ? exists : super.exists(id);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

    @Override
	public List<T> findAll() {
        final String idskey = BeanHelper.key(keyspace(), "findAll", null, null);
		try {
            final List<String> entitykeys = entityKeys(idskey);
		    if(!CollectionUtils.isEmpty(entitykeys)) {
                final List<T> finalEntities = getHashData(entitykeys);

                if (!CollectionUtils.isEmpty(finalEntities) && !CollectionUtils.isEmpty(entitykeys)) {
                    return finalEntities;
                }
            }

			final List<T> ts = super.findAll();

			if(CollectionUtils.isEmpty(ts)){
				return ts;
			}

//            execMultiHashPipelined(ts, idskey);
            final List<String> ids = Lists.newArrayListWithCapacity(10);
//            final Map<String, Map<String, Object>> dataMap = new HashMap(10);
//            ts.iterator().forEachRemaining(entity -> {
//                ids.add(String.valueOf(entity.getId()));
//
//                dataMap.put(key(entity.getId()), BeanHelper.toHash(entity));
//            });
            final Map<String, Object> dataMap = new HashMap(10);
            ts.iterator().forEachRemaining(entity -> {
                ids.add(String.valueOf(entity.getId()));

                dataMap.put(key(entity.getId()), JsonHelper.serialize(entity));
            });

//            lettuceRedisAdapter.batchHmset(dataMap);
            lettuceRedisAdapter.batchSetex(dataMap);
            lettuceRedisAdapter.setexlist(idskey, ids);

			return ts;
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

    @Override
	public List<T> findAll(Iterable<ID> ids) {
    	if(ObjectUtils.isEmpty(ids) || !ids.iterator().hasNext()){
			return Lists.newArrayListWithCapacity(0);
		}

		if(ids.iterator().hasNext()){
			ids.iterator().forEachRemaining(id -> {
				if(null == id){
					throw new RuntimeException("The param ids can not be null!");
				}
			});
		}

        final String idskey = BeanHelper.key(keyspace(), "findAll", new String[]{"ids"}, new Object[]{ids});
        final List<String> idList = Lists.newArrayListWithCapacity(10);
        try {
            final List<String> entitykeys = entityKeys(idskey);
            final List<T> finalEntities = getHashData(ids, idList);

            if(!CollectionUtils.isEmpty(finalEntities) && !CollectionUtils.isEmpty(entitykeys)){
                return finalEntities;
            }

            final List<T> ts = super.findAll(ids);

            if(CollectionUtils.isEmpty(ts)){
                return ts;
            }

            execMultiHashData(ts, idskey, idList);
            return ts;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
	public List<T> findAll(final Sort sort) {
        return findAllByCache(sort, null);
	}

    @Override
	public Page<T> findAll(final Pageable pageable) {
        return findAllPageByCache(pageable, null);
	}

    @Override
	public T findOne(final Specification<T> spec) {
        final int conditionsHashcode = SerializeAdapter.serialize(spec).hashCode();
        final String idskey = BeanHelper.key(keyspace(), "findOne", new String[]{"spec"}, new Object[]{conditionsHashcode});
        try {
            final String entitykey = entityKey(idskey);
            if(StringUtils.isNotBlank(entitykey)) {
                final T entity = getOnlyOne(entitykey, clazz);

                if (!ObjectUtils.isEmpty(entity)) {
                    return entity;
                }
            }

            final T t = super.findOne(spec);

            if(null == t) {
                return t;
            }

            lettuceRedisAdapter.batchProcessAsync(new AsyncCallback() {
                @Override
                public void asyncInvoke(RedisAsyncCommands<String, Object> commands) {
//                    commands.hmset(key(t.getId()), BeanHelper.toHash(t));
                    final RedisStringAsyncCommands<String, Object> stringAsyncCommands = (RedisStringAsyncCommands<String, Object>)commands;
                    stringAsyncCommands.setex(key(t.getId()), lettuceProperties.getCacheLevel2Ttl(), JsonHelper.serialize(t));
                    stringAsyncCommands.setex(idskey, lettuceProperties.getCacheLevel2Ttl(), String.valueOf(t.getId()));
                }

                @Override
                public void asyncInvoke(RedisReactiveCommands<String, Object> commands) {
//                    commands.hmset(key(t.getId()), BeanHelper.toHash(t));
                    final RedisStringAsyncCommands<String, Object> stringAsyncCommands = (RedisStringAsyncCommands<String, Object>)commands;
                    stringAsyncCommands.setex(key(t.getId()), lettuceProperties.getCacheLevel2Ttl(), JsonHelper.serialize(t));
                    stringAsyncCommands.setex(idskey, lettuceProperties.getCacheLevel2Ttl(), String.valueOf(t.getId()));
                }
            });

            return t;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
	}

    @Override
	public List<T> findAll(final Specification<T> spec) {
        return findAllByCache(null, spec);
	}

    @Override
	public Page<T> findAll(final Specification<T> spec, final Pageable pageable) {
        return findAllPageByCache(pageable, spec);
	}

    @Override
	public List<T> findAll(final Specification<T> spec, final Sort sort) {
        return findAllByCache(sort, spec);
	}

	@Override
	public <S extends T> S findOne(final Example<S> example) {
        final int conditionsHashcode = SerializeAdapter.serialize(example).hashCode();
        final String idskey = BeanHelper.key(keyspace(), "findOne", new String[]{"spec"}, new Object[]{conditionsHashcode});
        try {
            final String entitykey = entityKey(idskey);
            if(StringUtils.isNotBlank(entitykey)) {
                final S entity = getOnlyOne(entitykey, BeanHelper.getClassGenericityParam(example));

                if (!ObjectUtils.isEmpty(entity)) {
                    return entity;
                }
            }

            final S s = super.findOne(example);

            if(null == s) {
                return s;
            }

            lettuceRedisAdapter.batchProcessAsync(new AsyncCallback() {
                @Override
                public void asyncInvoke(RedisAsyncCommands<String, Object> commands) {
//                    commands.hmset(key(s.getId()), BeanHelper.toHash(s));
                    final RedisStringAsyncCommands<String, Object> stringAsyncCommands = (RedisStringAsyncCommands<String, Object>)commands;
                    stringAsyncCommands.setex(key(s.getId()), lettuceProperties.getCacheLevel2Ttl(), JsonHelper.serialize(s));
                    stringAsyncCommands.setex(idskey, lettuceProperties.getCacheLevel2Ttl(), String.valueOf(s.getId()));
                }

                @Override
                public void asyncInvoke(RedisReactiveCommands<String, Object> commands) {
//                    commands.hmset(key(s.getId()), BeanHelper.toHash(s));
                    final RedisStringAsyncCommands<String, Object> stringAsyncCommands = (RedisStringAsyncCommands<String, Object>)commands;
                    stringAsyncCommands.setex(key(s.getId()), lettuceProperties.getCacheLevel2Ttl(), JsonHelper.serialize(s));
                    stringAsyncCommands.setex(idskey, lettuceProperties.getCacheLevel2Ttl(), String.valueOf(s.getId()));
                }
            });

            return s;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
	}

	@Override
	public <S extends T> long count(final Example<S> example) {
        final int conditionsHashcode = SerializeAdapter.serialize(example).hashCode();
        final String idskey = BeanHelper.key(keyspace(), "count", new String[]{"count"}, new Object[]{conditionsHashcode});

        try {
            final String countkey = countKey(idskey);
            if(StringUtils.isNotBlank(countkey)){
                final Long count = Long.valueOf(countkey);
                if(null != count) {
                    return count;
                }
            }

            final long l = super.count(example);

            lettuceRedisAdapter.setex(idskey, String.valueOf(l));

            return l;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
	}

	@Override
	public <S extends T> boolean exists(Example<S> example) {
        final int conditionsHashcode = SerializeAdapter.serialize(example).hashCode();
        final String idskey = BeanHelper.key(keyspace(), "exists", new String[]{"exists"}, new Object[]{conditionsHashcode});

        try {
            final Boolean exists = Boolean.valueOf(countKey(idskey));
            if(null != exists) {
                return exists;
            }

            final boolean l = super.exists(example);

            lettuceRedisAdapter.setex(idskey, String.valueOf(l));

            return l;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
	}

	@Override
	public <S extends T> List<S> findAll(final Example<S> example) {
        return findAllByCache(example, null);
	}

	@Override
	public <S extends T> List<S> findAll(final Example<S> example, final Sort sort) {
        return findAllByCache(example, sort);
	}

	@Override
	public <S extends T> Page<S> findAll(final Example<S> example, final Pageable pageable) {
        return findAllPageByCache(example, pageable);
	}

    @Override
	public long count() {
        int size = -1;
        final int eks = entityKeys().size();
        if(eks > -1){
            size = eks;
        }
		return size == -1 ? super.count() : size;
	}

    @Override
	public long count(final Specification<T> spec) {
        final int conditionsHashcode = SerializeAdapter.serialize(spec).hashCode();
        final String idskey = BeanHelper.key(keyspace(), "count", new String[]{"count"}, new Object[]{conditionsHashcode});
        try {
            final Long count = Long.valueOf(countKey(idskey));
            if(null != count) {
                return count;
            }

            final long l = super.count(spec);

            lettuceRedisAdapter.setex(idskey, String.valueOf(l));

            return l;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
	}

	@Transactional
    @Override
	public <S extends T> S save(final S entity) {
		try{
            final String key = key(entity.getId());
            final Boolean flag = lettuceRedisAdapter.exists(key);
            if(BooleanUtils.isTrue(flag)) {
                lettuceRedisAdapter.del(key);
            }

            super.save(entity);

            final String targetClassName = entity.getClass().getSimpleName().toLowerCase();
            final String delingkey = lettuceProperties.cacheLevel2Pre() + "deling:" + targetClassName;
            final List<Object> delingCacheKeys = lettuceRedisAdapter.smembers(delingkey);
            final List<String> delingCachedKeys = Lists.newArrayListWithCapacity(3);
            if(CollectionUtil.isNotEmpty(delingCacheKeys)) {
                delingCacheKeys.iterator().forEachRemaining(delingCacheKey -> {
                    delingCachedKeys.addAll(lettuceRedisAdapter.keys(String.valueOf(delingCacheKey)));
                });
            }
            final int size = delingCachedKeys.size();
            final String keyspace = keyspace(targetClassName);
            lettuceRedisAdapter.batchProcessAsync(new AsyncCallback() {
                @Override
                public void asyncInvoke(RedisAsyncCommands<String, Object> commands) {
//                    commands.hmset(key(entity.getId()), BeanHelper.toHash(entity));
                    final RedisStringAsyncCommands<String, Object> stringAsyncCommands = (RedisStringAsyncCommands<String, Object>)commands;
                    final RedisKeyAsyncCommands<String, Object> keyAsyncCommands = (RedisKeyAsyncCommands<String, Object>)commands;
                    stringAsyncCommands.setex(key(entity.getId()), lettuceProperties.getCacheLevel2Ttl(), JsonHelper.serialize(entity));

                    if(!CollectionUtils.isEmpty(delingCachedKeys)){
                        keyAsyncCommands.del(delingCachedKeys.toArray(new String[size]));
                    }else{
                        final List<String> entityKeys = lettuceRedisAdapter.keys(keyspace);
                        if(!CollectionUtils.isEmpty(entityKeys)) {
                            keyAsyncCommands.del(entityKeys.toArray(new String[entityKeys.size()]));
                        }
                    }
                }

                @Override
                public void asyncInvoke(RedisReactiveCommands<String, Object> commands) {
//                    commands.hmset(key(entity.getId()), BeanHelper.toHash(entity));
                    final RedisStringAsyncCommands<String, Object> stringAsyncCommands = (RedisStringAsyncCommands<String, Object>)commands;
                    final RedisKeyAsyncCommands<String, Object> keyAsyncCommands = (RedisKeyAsyncCommands<String, Object>)commands;
                    stringAsyncCommands.setex(key(entity.getId()), lettuceProperties.getCacheLevel2Ttl(), JsonHelper.serialize(entity));

                    if(!CollectionUtils.isEmpty(delingCachedKeys)){
                        keyAsyncCommands.del(delingCachedKeys.toArray(new String[size]));
                    }else{
                        final List<String> entityKeys = lettuceRedisAdapter.keys(keyspace);
                        if(!CollectionUtils.isEmpty(entityKeys)) {
                            keyAsyncCommands.del(entityKeys.toArray(new String[entityKeys.size()]));
                        }
                    }
                }
            });

			return entity;
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	@Transactional
    @Override
	public <S extends T> S saveAndFlush(final S entity) {
        try{
            final String key = key(entity.getId());
            final Boolean flag = lettuceRedisAdapter.exists(key);
            if(BooleanUtils.isTrue(flag)) {
                lettuceRedisAdapter.del(key);
            }

            super.saveAndFlush(entity);

            final String targetClassName = entity.getClass().getSimpleName().toLowerCase();
            final String delingkey = lettuceProperties.cacheLevel2Pre() + "deling:" + targetClassName;
            final List<Object> delingCacheKeys = lettuceRedisAdapter.smembers(delingkey);
            final List<String> delingCachedKeys = Lists.newArrayListWithCapacity(3);
            if(CollectionUtil.isNotEmpty(delingCacheKeys)) {
                delingCacheKeys.iterator().forEachRemaining(delingCacheKey -> {
                    delingCachedKeys.addAll(lettuceRedisAdapter.keys(String.valueOf(delingCacheKey)));
                });
            }
            final int size = delingCachedKeys.size();
            final String keyspace = keyspace(targetClassName);
            lettuceRedisAdapter.batchProcessAsync(new AsyncCallback() {
                @Override
                public void asyncInvoke(RedisAsyncCommands<String, Object> commands) {
//                    commands.hmset(key(entity.getId()), BeanHelper.toHash(entity));
                    final RedisStringAsyncCommands<String, Object> stringAsyncCommands = (RedisStringAsyncCommands<String, Object>)commands;
                    final RedisKeyAsyncCommands<String, Object> keyAsyncCommands = (RedisKeyAsyncCommands<String, Object>)commands;
                    stringAsyncCommands.setex(key(entity.getId()), lettuceProperties.getCacheLevel2Ttl(), JsonHelper.serialize(entity));

                    if(!CollectionUtils.isEmpty(delingCachedKeys)){
                        keyAsyncCommands.del(delingCachedKeys.toArray(new String[size]));
                    }else{
                        final List<String> entityKeys = lettuceRedisAdapter.keys(keyspace);
                        if(!CollectionUtils.isEmpty(entityKeys)) {
                            keyAsyncCommands.del(entityKeys.toArray(new String[entityKeys.size()]));
                        }
                    }
                }

                @Override
                public void asyncInvoke(RedisReactiveCommands<String, Object> commands) {
//                    commands.hmset(key(entity.getId()), BeanHelper.toHash(entity));
                    final RedisStringAsyncCommands<String, Object> stringAsyncCommands = (RedisStringAsyncCommands<String, Object>)commands;
                    final RedisKeyAsyncCommands<String, Object> keyAsyncCommands = (RedisKeyAsyncCommands<String, Object>)commands;
                    stringAsyncCommands.setex(key(entity.getId()), lettuceProperties.getCacheLevel2Ttl(), JsonHelper.serialize(entity));

                    if(!CollectionUtils.isEmpty(delingCachedKeys)){
                        keyAsyncCommands.del(delingCachedKeys.toArray(new String[size]));
                    }else{
                        final List<String> entityKeys = lettuceRedisAdapter.keys(keyspace);
                        if(!CollectionUtils.isEmpty(entityKeys)) {
                            keyAsyncCommands.del(entityKeys.toArray(new String[entityKeys.size()]));
                        }
                    }
                }
            });

            return entity;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
	}

	@Transactional
    @Override
	public <S extends T> List<S> save(final Iterable<S> entities) {
        try{
            this.saveByJpa(entities);

            putAllExtToCache(entities);

            final String targetClassName = BeanHelper.getClassName(entities);
            final String delingkey = lettuceProperties.cacheLevel2Pre() + "deling:" + targetClassName;
            final List<Object> delingCacheKeys = lettuceRedisAdapter.smembers(delingkey);
            final List<String> delingCachedKeys = Lists.newArrayListWithCapacity(3);
            if(CollectionUtil.isNotEmpty(delingCacheKeys)) {
                delingCacheKeys.iterator().forEachRemaining(delingCacheKey -> {
                    delingCachedKeys.addAll(lettuceRedisAdapter.keys(String.valueOf(delingCacheKey)));
                });
            }
            final int size = delingCachedKeys.size();
            final String keyspace = keyspace(targetClassName);
            if(!CollectionUtils.isEmpty(delingCachedKeys)){
                lettuceRedisAdapter.del(delingCachedKeys.toArray(new String[size]));
            }else{
                final List<String> entityKeys = lettuceRedisAdapter.keys(keyspace);
                if(!CollectionUtils.isEmpty(entityKeys)) {
                    lettuceRedisAdapter.del(entityKeys.toArray(new String[entityKeys.size()]));
                }
            }

            return (List<S>)entities;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
	}

    private <S extends T> List<S> saveByJpa(final Iterable<S> entities) {
        final List<S> result = new ArrayList();
        if (entities == null) {
            return result;
        } else {
            final Iterator<S> var3 = entities.iterator();

            while(var3.hasNext()) {
                final S entity = var3.next();
                result.add(super.save(entity));
            }

            return result;
        }
    }

	@Transactional
    @Override
	public void flush() {
        super.flush();
	}


	private String keyspace(){
		return lettuceProperties.cacheLevel2Pre() + clazzName;
	}

    private String keyspace(String targetClassName){
        return lettuceProperties.cacheLevel2Pre() + targetClassName + ":finds:*";
    }

	private String key(final ID id){
		return keyspace() + ":ids:" + id;
	}

	private String key(final String id){
		return keyspace() + ":ids:" + id;
	}

	private List<String> entityKeys() {
		return lettuceRedisAdapter.keys(keyspace() + ":ids:*");
	}

    private String countKey(final String key){
        final Boolean hasKey = lettuceRedisAdapter.exists(key);
        if(BooleanUtils.isFalse(hasKey)){
            return null;
        }

        return lettuceRedisAdapter.get(key, String.class);
    }

    private String entityKey(final String key){
        final Boolean hasKey = lettuceRedisAdapter.exists(key);
        if(BooleanUtils.isFalse(hasKey)){
            return null;
        }

        return lettuceRedisAdapter.get(key, String.class);
    }

	private List<String> entityKeys(final String key){
        final Boolean hasKey = lettuceRedisAdapter.exists(key);
		if(BooleanUtils.isFalse(hasKey)){
			return null;
		}

		return lettuceRedisAdapter.getlist(key, String.class);
	}

    private <X> X getOnlyOne(final String key, final Class<X> targetClass){
        final Boolean hasKey = lettuceRedisAdapter.exists(key);
        if(BooleanUtils.isFalse(hasKey)){
            return null;
        }

//        final Map<String, Object> map = lettuceRedisAdapter.hgetall(key);
//        return BeanHelper.fromHash(map, targetClass);
        final String entity = lettuceRedisAdapter.get(key, String.class);
        return JsonHelper.deserialize(entity, targetClass);
    }

    private List<T> getEntities(final List<Map<String, Object>> result){
        final List<T> list = Lists.newArrayListWithCapacity(result.size());
        if(CollectionUtils.isEmpty(result)){
            return list;
        }

        result.iterator().forEachRemaining(map -> {
            final T entity = BeanHelper.fromHash(map, clazz);
			if (Objects.nonNull(entity) && Objects.nonNull(entity.getId())) {
				list.add(entity);
			}
        });

        return list;
    }

    private List<T> getNewEntities(final List<String> result){
        final List<T> list = Lists.newArrayListWithCapacity(result.size());
        if(CollectionUtils.isEmpty(result)) {
            return list;
        }

        result.iterator().forEachRemaining(t -> {
            final T entity = JsonHelper.deserialize(t, clazz);
            if (Objects.nonNull(entity) && Objects.nonNull(entity.getId())) {
                list.add(entity);
            }
        });

        return list;
    }

    private <S extends T> List<S> getExtEntities(final List<Map<String, Object>> result, final Class<S> targetClass){
        final List<S> list = Lists.newArrayListWithCapacity(result.size());
        result.stream().filter(p->null!=p).forEach(map -> {
            final S entity = BeanHelper.fromHash(map, targetClass);
            if (Objects.nonNull(entity)) {
                list.add(entity);
            }
        });

        return list;
    }

    private <S extends T> List<S> getNewExtEntities(final List<String> result, final Class<S> targetClass){
        final List<S> list = Lists.newArrayListWithCapacity(result.size());

        result.stream().filter(p->null!=p).forEach(t -> {
            final S entity = JsonHelper.deserialize(t, targetClass);
            if (Objects.nonNull(entity) && Objects.nonNull(entity.getId())) {
                list.add(entity);
            }
        });

        return list;
    }

	private List<T> findAllByCache(final Sort sort, final Specification<T> spec) {
        final String[] paramnames = new String[2];
        final Object[] paramvals = new Object[2];
        if(!ObjectUtils.isEmpty(spec)){
            paramnames[0] = "spec";
            paramvals[0] = SerializeAdapter.serialize(spec).hashCode();
        }
        if(!ObjectUtils.isEmpty(sort)){
            paramnames[1] = "sort";
            paramvals[1] = SerializeAdapter.serialize(sort).hashCode();
        }

        final String idskey = BeanHelper.key(keyspace(), "findAll", paramnames, paramvals);
        try {
            List<String> entitykeys = entityKeys(idskey);
            if(!CollectionUtils.isEmpty(entitykeys)) {
                entitykeys = entitykeys.stream().map(id -> {
                    return key(id);
                }).collect(Collectors.toList());
//                final List<T> finalEntities = getEntities(lettuceRedisAdapter.batchHgetall(entitykeys));
                final List<T> finalEntities = getNewEntities(lettuceRedisAdapter.batchGet(entitykeys, String.class));

				if (!CollectionUtils.isEmpty(finalEntities) && !CollectionUtils.isEmpty(entitykeys)) {
                    return finalEntities;
                }
			}

            List<T> result = null;
            if(!ObjectUtils.isEmpty(spec) && ObjectUtils.isEmpty(sort)){
                result = super.findAll(spec, (Sort) null);
            }
            if(!ObjectUtils.isEmpty(sort) && ObjectUtils.isEmpty(spec)){
                result = super.findAll((Specification<T>) null, sort);
            }
            if(!ObjectUtils.isEmpty(sort) && !ObjectUtils.isEmpty(spec)){
                result = super.findAll(spec, sort);
            }
			if(CollectionUtils.isEmpty(result)){
				return result;
			}

//            execMultiHashPipelined(result, idskey);
            final List<String> ids = Lists.newArrayListWithCapacity(10);
//            final Map<String, Map<String, Object>> dataMap = new HashMap(10);
//            result.iterator().forEachRemaining(entity -> {
//                ids.add(String.valueOf(entity.getId()));
//
//                dataMap.put(key(entity.getId()), BeanHelper.toHash(entity));
//            });
            final Map<String, Object> dataMap = new HashMap(10);
            result.iterator().forEachRemaining(entity -> {
                ids.add(String.valueOf(entity.getId()));

                dataMap.put(key(entity.getId()), JsonHelper.serialize(entity));
            });

//            lettuceRedisAdapter.batchHmset(dataMap);
            lettuceRedisAdapter.batchSetex(dataMap);
            lettuceRedisAdapter.setexlist(idskey, ids);

			return result;
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

    private <S extends T> List<S> findAllByCache(final Example<S> example, final Sort sort) {
        final String[] paramnames = new String[2];
        final Object[] paramvals = new Object[2];
        if(!ObjectUtils.isEmpty(example)){
            paramnames[0] = "example";
            paramvals[0] = SerializeAdapter.serialize(example).hashCode();
        }
        if(!ObjectUtils.isEmpty(sort)){
            paramnames[1] = "sort";
            paramvals[1] = SerializeAdapter.serialize(sort).hashCode();
        }

        final String idskey = BeanHelper.key(keyspace(), "findAll", paramnames, paramvals);
        try {
            List<String> entitykeys = entityKeys(idskey);
            if(!CollectionUtils.isEmpty(entitykeys)) {
                entitykeys = entitykeys.stream().map(id -> {
                    return key(id);
                }).collect(Collectors.toList());
//                final List<S> finalEntities = getExtEntities(lettuceRedisAdapter.batchHgetall(entitykeys), BeanHelper.getClassGenericityParam(example));
                final List<S> finalEntities = getNewExtEntities(lettuceRedisAdapter.batchGet(entitykeys, String.class), BeanHelper.getClassGenericityParam(example));

                if (!CollectionUtils.isEmpty(finalEntities) && !CollectionUtils.isEmpty(entitykeys)) {
                    return finalEntities;
                }
            }

            List<S> result = null;
            if(!ObjectUtils.isEmpty(example) && ObjectUtils.isEmpty(sort)){
                result = super.findAll(example, (Sort) null);
            }
            if(!ObjectUtils.isEmpty(example) && !ObjectUtils.isEmpty(sort)){
                result = super.findAll(example, sort);
            }
            if(CollectionUtils.isEmpty(result)){
                return result;
            }

//            execMultiHashPipelined(idskey, result);
            final List<String> ids = Lists.newArrayListWithCapacity(10);
//            final Map<String, Map<String, Object>> dataMap = new HashMap(10);
//            result.iterator().forEachRemaining(entity -> {
//                ids.add(String.valueOf(entity.getId()));
//
//                dataMap.put(key(entity.getId()), BeanHelper.toHash(entity));
//            });
            final Map<String, Object> dataMap = new HashMap(10);
            result.iterator().forEachRemaining(entity -> {
                ids.add(String.valueOf(entity.getId()));

                dataMap.put(key(entity.getId()), JsonHelper.serialize(entity));
            });

//            lettuceRedisAdapter.batchHmset(dataMap);
            lettuceRedisAdapter.batchSetex(dataMap);
            lettuceRedisAdapter.setexlist(idskey, ids);

            return result;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private Page<T> findAllPageByCache(final Pageable pageable, final Specification<T> spec) {
        final String[] paramnames = new String[2];
        final Object[] paramvals = new Object[2];
        if(!ObjectUtils.isEmpty(spec)){
            paramnames[0] = "spec";
            paramvals[0] = SerializeAdapter.serialize(spec).hashCode();
        }
        if(!ObjectUtils.isEmpty(pageable)){
            paramnames[1] = "pageable";
            paramvals[1] = SerializeAdapter.serialize(pageable).hashCode();
        }

        final String idskey = BeanHelper.key(keyspace(), "findAll", paramnames, paramvals);
        final String countkey = idskey+":count";
        try {
            List<String> entitykeys = entityKeys(idskey);
            if(!CollectionUtils.isEmpty(entitykeys)) {
                entitykeys = entitykeys.stream().map(id -> {
                    return key(id);
                }).collect(Collectors.toList());
//                final List<T> finalEntities = getEntities(lettuceRedisAdapter.batchHgetall(entitykeys));
                final List<T> finalEntities = getNewEntities(lettuceRedisAdapter.batchGet(entitykeys, String.class));

                if (!CollectionUtils.isEmpty(finalEntities) && !CollectionUtils.isEmpty(entitykeys)) {
                    final long countval = Long.valueOf(lettuceRedisAdapter.get(countkey, String.class)).longValue();
                    return new PageImpl(finalEntities, pageable, countval);
                }
            }

            Page<T> result = null;
            if(!ObjectUtils.isEmpty(pageable) && ObjectUtils.isEmpty(spec)){
                result = super.findAll((Specification<T>) null, pageable);
            }
            if(!ObjectUtils.isEmpty(pageable) && !ObjectUtils.isEmpty(spec)){
                result = super.findAll(spec, pageable);
            }
            if(CollectionUtils.isEmpty(result.getContent())){
                return result;
            }

            final List<String> ids = Lists.newArrayListWithCapacity(10);

//            final Map<String, Map<String, Object>> dataMap = new HashMap(10);
//            result.getContent().iterator().forEachRemaining(entity -> {
//                ids.add(String.valueOf(entity.getId()));
//
//                dataMap.put(key(entity.getId()), BeanHelper.toHash(entity));
//            });
            final Map<String, Object> dataMap = new HashMap(10);
            result.getContent().iterator().forEachRemaining(entity -> {
                ids.add(String.valueOf(entity.getId()));

                dataMap.put(key(entity.getId()), JsonHelper.serialize(entity));
            });

//            lettuceRedisAdapter.batchHmset(dataMap);
            lettuceRedisAdapter.batchSetex(dataMap);
            lettuceRedisAdapter.setexlist(idskey, ids);
            lettuceRedisAdapter.setex(countkey, String.valueOf(result.getTotalElements()));

            return result;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private <S extends T> Page<S> findAllPageByCache(final Example<S> example, final Pageable pageable) {
        final String[] paramnames = new String[2];
        final Object[] paramvals = new Object[2];
        if(!ObjectUtils.isEmpty(example)){
            paramnames[0] = "example";
            paramvals[0] = SerializeAdapter.serialize(example).hashCode();
        }
        if(!ObjectUtils.isEmpty(pageable)){
            paramnames[1] = "pageable";
            paramvals[1] = SerializeAdapter.serialize(pageable).hashCode();
        }

        final String idskey = BeanHelper.key(keyspace(), "findAll", paramnames, paramvals);
        final String countkey = idskey+":count";
        try {
            List<String> entitykeys = entityKeys(idskey);
            if(!CollectionUtils.isEmpty(entitykeys)) {
                entitykeys = entitykeys.stream().map(id -> {
                    return key(id);
                }).collect(Collectors.toList());
//                final List<S> finalEntities = getExtEntities(lettuceRedisAdapter.batchHgetall(entitykeys), BeanHelper.getClassGenericityParam(example));
                final List<S> finalEntities = getNewExtEntities(lettuceRedisAdapter.batchGet(entitykeys, String.class), BeanHelper.getClassGenericityParam(example));

                if (!CollectionUtils.isEmpty(finalEntities) && !CollectionUtils.isEmpty(entitykeys)) {
                    final long countval = Long.valueOf(lettuceRedisAdapter.get(countkey, String.class)).longValue();
                    return new PageImpl(finalEntities, pageable, countval);
                }
            }

            final Page<S> result = super.findAll(example, pageable);
            if(CollectionUtils.isEmpty(result.getContent())){
                return result;
            }

            final List<String> ids = Lists.newArrayListWithCapacity(10);
//            final Map<String, Map<String, Object>> dataMap = new HashMap(10);
//            result.getContent().iterator().forEachRemaining(entity -> {
//                ids.add(String.valueOf(entity.getId()));
//
//                dataMap.put(key(entity.getId()), BeanHelper.toHash(entity));
//            });
            final Map<String, Object> dataMap = new HashMap(10);
            result.getContent().iterator().forEachRemaining(entity -> {
                ids.add(String.valueOf(entity.getId()));

                dataMap.put(key(entity.getId()), JsonHelper.serialize(entity));
            });

//            lettuceRedisAdapter.batchHmset(dataMap);
            lettuceRedisAdapter.batchSetex(dataMap);
            lettuceRedisAdapter.setexlist(idskey, ids);
            lettuceRedisAdapter.setex(countkey, String.valueOf(result.getTotalElements()));

            return result;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private List<T> getHashData(List<String> entitykeys){
        entitykeys = entitykeys.stream().map(id -> {
            return key(id);
        }).collect(Collectors.toList());
//        return getEntities(lettuceRedisAdapter.batchHgetall(entitykeys));
        return getNewEntities(lettuceRedisAdapter.batchGet(entitykeys, String.class));
    }

    private List<T> getHashData(final Iterable<ID> ids, final List<String> idList){
        final List<String> entitykeys = Lists.newArrayListWithCapacity(10);
        ids.iterator().forEachRemaining(id -> {
            if(null != id) {
                idList.add(id + "");
                entitykeys.add(key(id));
            }
        });
//        return getEntities(lettuceRedisAdapter.batchHgetall(entitykeys));
        return getNewEntities(lettuceRedisAdapter.batchGet(entitykeys, String.class));
    }

    private void execMultiHashData(final List<T> ts, final String idskey, final List<String> idList){

        putAllToCache(ts);

        lettuceRedisAdapter.setexlist(idskey, idList);
    }

    private void putAllToCache(final Iterable<T> entities){
//        final Map<String, Map<String, Object>> dataMap = new HashMap(10);
//        entities.iterator().forEachRemaining(entity -> {
//
//            dataMap.put(key(entity.getId()), BeanHelper.toHash(entity));
//        });
        final Map<String, Object> dataMap = new HashMap(10);
        entities.iterator().forEachRemaining(entity -> {

            dataMap.put(key(entity.getId()), JsonHelper.serialize(entity));
        });

//        lettuceRedisAdapter.batchHmset(dataMap);
        lettuceRedisAdapter.batchSetex(dataMap);
    }

    private <S extends T> void putAllExtToCache(final Iterable<S> entities){
//        final Map<String, Map<String, Object>> dataMap = new HashMap(10);
//        entities.iterator().forEachRemaining(entity -> {
//
//            dataMap.put(key(entity.getId()), BeanHelper.toHash(entity));
//        });
        final Map<String, Object> dataMap = new HashMap(10);
        entities.iterator().forEachRemaining(entity -> {

            dataMap.put(key(entity.getId()), JsonHelper.serialize(entity));
        });

//        lettuceRedisAdapter.batchHmset(dataMap);
        lettuceRedisAdapter.batchSetex(dataMap);
    }
}